const crypto = require('crypto');
const { errors } = require('arsenal');

/** Class exposing common createDataKey,
createDecipher and createCipher functions. */
class Common {
    static _algorithm() {
        return 'aes-256-ctr';
    }

    /* AES-256 Key */
    static _keySize() {
        return 32;
    }

    /* IV is 128bit for AES-256-CTR */
    static _IVSize() {
        return 16;
    }

    /* block size is 128bit for AES-256-CTR */
    static _aesBlockSize() {
        return 16;
    }

    /**
    Creates data key to encrypt and decrypt the actual data (which data
    key is ciphered and deciphered by the appliance). The encrypted data key
    is stored in the object's metadata. We also use this function to create
    the bucket key for the file and in memory implementations.
    @return {buffer} - random key
    */
    static createDataKey() {
        return Buffer.from(crypto.randomBytes(this._keySize()));
    }

    /**
     *
     * @param {buffer} derivedIV - the stringified bucket
     * @param {number} counter - quotient of the offset and blocksize
     * @return {buffer} - the incremented IV
     */
    static _incrementIV(derivedIV, counter) {
        const newIV = derivedIV;
        const len = derivedIV.length;
        let i = len - 1;
        let ctr = counter;
        while (ctr !== 0) {
            const mod = (ctr + newIV[i]) % 256;
            ctr = Math.floor((ctr + newIV[i]) / 256);
            newIV[i] = mod;
            i -= 1;
            if (i < 0) {
                i = len - 1;
            }
        }
        return newIV;
    }

     /**
      * Derive key to use in cipher
      * @param {number} cryptoScheme - cryptoScheme being used
      * @param {buffer} dataKey - the unencrypted key (either from the
      * appliance on a get or originally generated by kms in the case of a put)
      * @param {object} log - logger object
      * @param {function} cb - cb from createDecipher
      * @returns {undefined}
      * @callback called with (err, derivedKey, derivedIV)
      */
    static _deriveKey(cryptoScheme, dataKey, log, cb) {
        if (cryptoScheme <= 1) {
            /* we are not storing hashed human password.
             * It's a random key, so 1 iteration and
             * a fixed salt is enough for our usecase.
             * don't change the salt, the iteration number
             * or the digest algorithm (sha1 here) without
             * bumping the cryptoScheme number saved in the object
             * metadata along with the dataKey.
             */
            const salt = Buffer.from('ItsTasty', 'utf8');
            const iterations = 1;
            return crypto.pbkdf2(
                dataKey, salt, iterations,
                this._keySize(), 'sha1', (err, derivedKey) => {
                    if (err) {
                        log.error('pbkdf2 function failed on key derivation',
                                  { error: err });
                        cb(errors.InternalError);
                        return;
                    }
                    crypto.pbkdf2(
                        derivedKey, salt, iterations,
                        this._IVSize(), 'sha1', (err, derivedIV) => {
                            if (err) {
                                log.error(
                                    'pbkdf2 function failed on IV derivation',
                                    { error: err });
                                return cb(errors.InternalError);
                            }
                            // derivedKey is the actual data encryption or
                            // decryption key used in the AES ctr cipher
                            return cb(null, derivedKey, derivedIV);
                        });
                });
        }
        log.error('Unknown cryptographic scheme', { cryptoScheme });
        return cb(errors.InternalError);
    }

     /**
      * createDecipher
      * @param {number} cryptoScheme - cryptoScheme being used
      * @param {buffer} dataKey - the unencrypted key (either from the
      * appliance on a get or originally generated by kms in the case of a put)
      * @param {number} offset - offset
      * @param {object} log - logger object
      * @param {function} cb - cb from external call
      * @returns {undefined}
      * @callback called with (err, decipher: ReadWritable.stream)
      */
    static createDecipher(cryptoScheme, dataKey, offset, log, cb) {
        this._deriveKey(
            cryptoScheme, dataKey, log,
            (err, derivedKey, derivedIV) => {
                if (err) {
                    log.debug('key derivation failed', { error: err });
                    return cb(err);
                }
                const aesBlockSize = this._aesBlockSize();
                const blocks = Math.floor(offset / aesBlockSize);
                const toSkip = offset % aesBlockSize;
                const iv = this._incrementIV(derivedIV, blocks);
                const cipher = crypto.createDecipheriv(this._algorithm(),
                                                       derivedKey, iv);
                if (toSkip) {
                    /* Above, we advanced to the latest boundary not
                    greater than the offset amount. Here we advance by
                    the toSkip amount if necessary. */
                    const dummyBuffer = Buffer.alloc(toSkip);
                    cipher.write(dummyBuffer);
                    cipher.read();
                }
                return cb(null, cipher);
            });
    }

    /**
     * createCipher (currently same as createDecipher function above.  this
     * wrapper is included to preserve flexibility)
     * @param {number} cryptoScheme - cryptoScheme being used
     * @param {buffer} dataKey - the unencrypted key (either from the
     * appliance on a get or originally generated by kms in the case of a put)
     * @param {number} offset - offset
     * @param {object} log - logger object
     * @param {function} cb - cb from external call
     * @returns {undefined}
     * @callback called with (err, cipher: ReadWritable.stream)
     */
    static createCipher(cryptoScheme, dataKey, offset, log, cb) {
        /* aes-256-ctr decipher is both ways */
        this.createDecipher(cryptoScheme, dataKey, offset, log, cb);
    }
}

module.exports = Common;
